#include "CLogin.h"
#include <wx/msgdlg.h>
#include "Utils.h"
#include "OAuth.h"
#include <stdexcept>
#include "tinyxml.h"
#include "tinystr.h"

CLogin::CLogin()
:mHttpReq(true)
,mOauth(Oauth::GetInstance())
,mLoginState(OAUTH_STATE_INVALID)
,mLoginStatePrevious(OAUTH_STATE_INVALID)
,mpStateMachineCS(new wxCriticalSection())
,mpParserCS(new wxCriticalSection())
,mpChangeStateCS(new wxCriticalSection())
,mpXMLParserCS(new wxCriticalSection())
,mLoginError(INVALID_ERROR_TYPE)
{
    Init();
}

CLogin:: ~CLogin()
{
    Destroy();
}

void CLogin::Init()
{
    //changeStateTo(OAUTH_REQEUST_TOKEN_PREPARE);
}

void CLogin::Destroy()
{
    if(NULL != mpStateMachineCS) delete mpStateMachineCS;
    if(NULL != mpParserCS) delete mpParserCS;
    if(NULL != mpChangeStateCS) delete mpChangeStateCS;
    if(NULL != mpXMLParserCS) delete mpXMLParserCS;
    mpStateMachineCS = NULL;
    mpParserCS = NULL;
    mpChangeStateCS = NULL;
    mpXMLParserCS = NULL;
}

bool CLogin::isLogin()
{
    if( !sysParamDB.getSystemParam("user_id").IsEmpty() &&
        !sysParamDB.getSystemParam("friends_count").IsEmpty() &&
        !sysParamDB.getSystemParam("followers_count").IsEmpty() )
       {
            return mOauth.setAccessPair(sysParamDB.getSystemParam("access_token"), sysParamDB.getSystemParam("access_secret"));
       }

    return false;
}

bool CLogin::prepareForReqToken()
{
    mOauth.resetData();
    mOauth.enableCallback();
    mOauth.signRequest(mOauth.getBaseString(REQUEST_TOKEN_API));
    mHttpReq.setOauthHeader(mOauth.getOauthHeader(), HTTP_METHOD_GET);  //set customized header , avoid auto-gen access_token header
    #ifdef LOGIN_DEBUG_LOG
    LOGMAN.logInfo(wxString("BaseString : ")<<mOauth.getBaseString(REQUEST_TOKEN_API));
    LOGMAN.logInfo(wxString("OauthHeader : ")<<mOauth.getOauthHeader());
    #endif
    return true;
}

bool CLogin::prepareForAccessToken(const wxString& _PIN)
{
    wxString reqToken =sysParamDB.getSystemParam("request_token");
    wxString reqSecret =sysParamDB.getSystemParam("request_secret");
    bool checkResult = ! (reqToken.IsEmpty() || reqSecret.IsEmpty());

    assert(checkResult);
    if(! checkResult)
    {
        #ifdef LOGIN_LOG
        LOGMAN.logError("Error: request token or secret is empty when prepareForAccessToken");
        #endif
        return false;
    }

    mOauth.resetData();
    mOauth.setToken(reqToken);
    mOauth.setSecret(reqSecret);
    mOauth.setVerifier(_PIN);

    mOauth.signRequest(mOauth.getBaseString(ACCESS_TOKEN_API));
    mHttpReq.setOauthHeader(mOauth.getOauthHeader(), HTTP_METHOD_GET); //set customized header , avoid auto-gen access_token header
    #ifdef LOGIN_DEBUG_LOG
    LOGMAN.logInfo(wxString("BaseString : ")<<mOauth.getBaseString(ACCESS_TOKEN_API));
    LOGMAN.logInfo(wxString("OauthHeader : ")<<mOauth.getOauthHeader());
    #endif
    return true;
}

inline bool CLogin::parseReceivedData(wxString rawStr)
{
    wxCriticalSectionLocker parserCSLocker(*mpParserCS);
    mHttpReceivedData.clear();

    while(!rawStr.IsEmpty())
    {
        int seperatorIndex = rawStr.Find('&');
        if(seperatorIndex!=wxNOT_FOUND)
        {
            wxString paramsUnit=rawStr.Left(seperatorIndex);
            mHttpReceivedData[paramsUnit.BeforeFirst('=')] = paramsUnit.AfterFirst('=');
            rawStr = rawStr.Right(rawStr.Length()-seperatorIndex-1);
        }
        else
        {
            mHttpReceivedData[rawStr.BeforeFirst('=')] = rawStr.AfterFirst('=');
            break;
        }
    }

    return !mHttpReceivedData.empty();
}

inline void CLogin::changeStateTo(LoginStateType _nextState)
{
    wxCriticalSectionLocker changeStateCSLocker(*mpChangeStateCS);
    mLoginStatePrevious = mLoginState;
    mLoginState = _nextState;
}

bool CLogin::parseXMLUserInfo(const wxString& xmlRawStr)
{
    wxCriticalSectionLocker XMLparserCSLocker(*mpXMLParserCS);

    DOM* xmlDoc = new DOM();
    xmlDoc->Parse(xmlRawStr.ToUTF8());

    Node* root = NULL;
    Node* leafNode = NULL;

    using std::runtime_error;

    try
    {
        root = xmlDoc->RootElement();
        if(root!=NULL)    //root == <user>
        {
            leafNode = root->FirstChild();
            if(leafNode !=NULL)   // leafNode == <id>
            {
                wxString tag(wxT(""));
                wxString textValue(wxT(""));
                mHttpReceivedData.clear();

                do
                {
                    if(wxString(leafNode->Value()) == _T("id") ||
                       wxString(leafNode->Value()) == _T("friends_count") ||
                       wxString(leafNode->Value()) == _T("followers_count"))
                    {
                        tag = tag.FromUTF8(leafNode->Value());
                        textValue = textValue.FromUTF8(leafNode->ToElement()->GetText());
                        mHttpReceivedData[tag] = textValue;
                        #ifdef LOGIN_DEBUG
                                wxMessageBox(tag+ " : "+ mHttpReceivedData[tag]) ;
                        #endif
                        #ifdef LOGIN_DEBUG_LOG
                                LOGMAN.logInfo(tag+ " : "+ mHttpReceivedData[tag]) ;
                        #endif
                    }
                    leafNode = leafNode->NextSibling();
                }while( leafNode != NULL );

            }else
            {
                throw runtime_error("leaf Node is none");
            }//End of if(userNode)

        }else
        {
            throw runtime_error("XML doc is null");
        }// End of if(root)

    }catch(runtime_error& e)
    {
        #ifdef LOGIN_DEBUG
            wxMessageBox(e.what());
        #endif
        #ifdef LOGIN_LOG
            LOGMAN.logError(e.what());
        #endif

        root = NULL;
        leafNode = NULL;
        delete xmlDoc;
        xmlDoc = NULL;
        return false;
    }//End of try

    root = NULL;
    leafNode = NULL;
    delete xmlDoc;
    xmlDoc = NULL;
    return true;
}//End of void CUser::startParsing()


bool CLogin::Finalize()
{
    if(mHttpReceivedData.count("id")>0                       && // [id] is from <id> in XML info, while key string in DB is user_id
       mHttpReceivedData.count("followers_count")>0    &&
       mHttpReceivedData.count("friends_count")>0       )
    {
        wxString uIDfromAccessToken = sysParamDB.getSystemParam("user_id");
        assert(mHttpReceivedData["id"] == uIDfromAccessToken);
        if(mHttpReceivedData["id"] == uIDfromAccessToken)
        {
            //store "" is just OK , actually it won't be stored
            sysParamDB.setSystemParam("followers_count", mHttpReceivedData["followers_count"]);
            sysParamDB.setSystemParam("friends_count", mHttpReceivedData["friends_count"]);
            return true;
        }
        else
        {
            //should not happen
            #ifdef LOGIN_LOG
            LOGMAN.logError("When login finalize , user_id in XMLinfo differs from that along with access_token");
            #endif
        }
    }
    else
    {
        //May not happen , otherwise it will raise an error in XML parser
        #ifdef LOGIN_LOG
        LOGMAN.logError("When login finalize , one of the required info not found.");
        #endif
    }

    sysParamDB.deleteSystemParam("user_id");
    sysParamDB.deleteSystemParam("access_token");
    sysParamDB.deleteSystemParam("access_secret");
    return false;
}



/*
// For the non-state machine design,
// it's hard to implement a multiple retry when http 401.
// But this is must-have when you contact such a poor server

bool CLogin::getToken()
{
    using namespace std;

    try
    {
        prepareForReqToken();
         if(mHttpReq.startReq(REQUEST_TOKEN_API))
        {
            if(parseReceivedData(mHttpReq.getResult()))
            {
                if(mHttpReceivedData.count("oauth_token")>0 && mHttpReceivedData.count("oauth_token_secret"))
                {
                    sysParamDB.setSystemParam("oauth_token", mHttpReceivedData["oauth_token"]);
                    sysParamDB.setSystemParam("oauth_token_secret", mHttpReceivedData["oauth_token_secret"]);
                }
                else
                {
                    throw runtime_error("Login terminated : requestToken saving failed");
                }
            }
            else
            {
                throw runtime_error("Login terminated : requestToken parsing failed");
            }
        }
        else
        {
            char buf[4];
            itoa(mHttpReq.httpErrCode, buf, 10);
            throw runtime_error(string("Login terminated when requestToken httpReq, error code").append(buf));
        }
    }
    catch(runtime_error& e)
    {
        #ifdef LOGIN_LOG
        LOGMAN.logError(e.what());
        #endif
        return false;
    }
}
*/

/********************    Class API   **********************/

///Use state machine for login multiple retry and state debug backtrace
bool CLogin::getToken(OauthTokenType tokenType, const wxString& PIN)
{
    wxCriticalSectionLocker stateMachineCSLocker(*mpStateMachineCS);
    wxString errorMsg("");
    mLoginError = UNKNOWN_ERROR;

    if(tokenType==REQUEST_TOKEN)
    {
        mOauth.resetData();
        changeStateTo(CLogin::OAUTH_REQEUST_TOKEN_PREPARE);
    }
    else if(tokenType==ACCESS_TOKEN)
    {
        changeStateTo(CLogin::OAUTH_ACCESS_TOKEN_PREPARE);
    }
    else
    {
        assert(false);
        return false;
    }



    while(true)
    {
        #ifdef LOGIN_DEBUG_LOG
        LOGMAN.logInfo(wxString("Login state at : ")<<mLoginState);
        #endif

        switch(mLoginState)
        {
            case OAUTH_STATE_INVALID:
            {
                #ifdef LOGIN_LOG
                LOGMAN.logError(wxString("Login terminated due to invalid state"));
                #endif
                return false;
            }
            case OAUTH_ERROR:
            {
                #ifdef LOGIN_LOG
                LOGMAN.logError(wxString("Login terminated at ")<<mLoginStatePrevious<<wxString(" : ")<<errorMsg);
                #endif
                return false;
            }

            /**************************************************/

            case OAUTH_REQEUST_TOKEN_PREPARE:
            {
                if(prepareForReqToken())
                {
                    changeStateTo(OAUTH_REQEUST_TOKEN_HTTPREQ);
                }
                else
                {
                    changeStateTo(OAUTH_ERROR);
                }

                break;
            }
            case OAUTH_REQEUST_TOKEN_HTTPREQ:
            {
                //TO DO : login retry. Oauth failure with high possibility when you login sina
                if(mHttpReq.startReq(REQUEST_TOKEN_API))
                {
                    changeStateTo(OAUTH_REQEUST_TOKEN_RESPONSE);
                }
                else
                {
                    if(mHttpReq.httpErrCode==mHttpReq.NOT_AUTHORIZED)
                    {
                        mLoginError = REQUEST_TOKEN_UNAUTHORIZED;
                    }
                    else if(mHttpReq.httpErrCode==-1 || mHttpReq.httpErrCode==0)
                    {
                        mLoginError = HTTP_DISCONNECTED;

                        #ifdef HTTP_WTF
                        if(mHttpReq.httpErrCode==0)
                            LOGMAN.logInfo(wxString("No respond URL : ")<<mHttpReq.errorInfo);
                        #endif
                    }

                    errorMsg = wxString("OAUTH_REQEUST_TOKEN_HTTPREQ, error code ")<<mHttpReq.httpErrCode;
                    changeStateTo(OAUTH_ERROR);
                }

                break;
            }
            case OAUTH_REQEUST_TOKEN_RESPONSE:
            {
                if(parseReceivedData(mHttpReq.getResult()))
                {
                    if(mHttpReceivedData.count("oauth_token")>0 && mHttpReceivedData.count("oauth_token_secret"))
                    {
                        sysParamDB.setSystemParam("request_token", mHttpReceivedData["oauth_token"]);
                        sysParamDB.setSystemParam("request_secret", mHttpReceivedData["oauth_token_secret"]);
                        changeStateTo(OAUTH_REQEUST_TOKEN_DONE);
                        #ifdef UNIT_TEST
                        //wxMessageBox(mHttpReq.getResult());
                        //wxLaunchDefaultBrowser(wxString("http://api.t.sina.com.cn/oauth/authorize?oauth_token=")<<mHttpReceivedData["oauth_token"]);
                        #endif
                    }
                    else
                    {
                        errorMsg = "requestToken saving failed";
                        mLoginError = REQUEST_TOKEN_UNRECOGNIZED_DATA;
                        changeStateTo(OAUTH_ERROR);
                    }
                }
                else
                {
                    errorMsg = "requestToken parsing failed";
                    mLoginError = REQUEST_TOKEN_UNRECOGNIZED_DATA;
                    changeStateTo(OAUTH_ERROR);
                }

                break;
            }
            case OAUTH_REQEUST_TOKEN_DONE:
            {
                //changeStateTo(OAUTH_STATE_INVALID);
                mLoginError = NO_ERROR;
                return true;
            }

            /**********************************************/

            case OAUTH_ACCESS_TOKEN_PREPARE:
            {
                if(prepareForAccessToken(PIN))
                {
                    changeStateTo(OAUTH_ACCESS_TOKEN_HTTPREQ);
                }
                else
                {
                    changeStateTo(OAUTH_ERROR);
                }

                break;
            }
            case OAUTH_ACCESS_TOKEN_HTTPREQ:
            {
                if(mHttpReq.startReq(ACCESS_TOKEN_API))
                {
                    changeStateTo(OAUTH_ACCESS_TOKEN_RESPONSE);
                }
                else
                {
                    if(mHttpReq.httpErrCode==mHttpReq.NOT_AUTHORIZED)
                    {
                        mLoginError = ACCESS_TOKEN_UNAUTHORIZED;
                    }
                    else if(mHttpReq.httpErrCode==-1 || mHttpReq.httpErrCode==0)
                    {
                        mLoginError = HTTP_DISCONNECTED;
                    }

                    errorMsg = wxString("OAUTH_ACCESS_TOKEN_HTTPREQ, error code ")<<mHttpReq.httpErrCode;
                    changeStateTo(OAUTH_ERROR);
                }

                break;
            }
            case OAUTH_ACCESS_TOKEN_RESPONSE:
            {
                #ifdef UNIT_TEST
                //wxMessageBox(mHttpReq.getResult());
                #endif

                if(parseReceivedData(mHttpReq.getResult()))
                {
                    if(mHttpReceivedData.count("oauth_token")>0 && mHttpReceivedData.count("oauth_token_secret")>0)
                    {
                        //save to DB
                        sysParamDB.setSystemParam("access_token", mHttpReceivedData["oauth_token"]);
                        sysParamDB.setSystemParam("access_secret", mHttpReceivedData["oauth_token_secret"]);
                        sysParamDB.setSystemParam("user_id", mHttpReceivedData["user_id"]);

                        //save as login info
                        mOauth.setAccessPair(mHttpReceivedData["oauth_token"], mHttpReceivedData["oauth_token_secret"]);

                        changeStateTo(OAUTH_ACCESS_TOKEN_DONE);
                    }
                    else
                    {
                        errorMsg = "accessToken saving failed";
                        mLoginError = ACCESS_TOKEN_UNRECOGNIZED_DATA;
                        changeStateTo(OAUTH_ERROR);
                    }
                }
                else
                {
                    errorMsg = "accessToken parsing failed";
                    mLoginError = ACCESS_TOKEN_UNRECOGNIZED_DATA;
                    changeStateTo(OAUTH_ERROR);
                }

                break;
            }
            case OAUTH_ACCESS_TOKEN_DONE:
            {
                mLoginError = NO_ERROR;
                return true;
            }

            default:
            {
                assert(false);
                return false;
            }
        }//end of switch case
    }//end of while
}//end of getToken


bool CLogin::getUserInfo(const wxString& accountVerifyAPI)
{
    mLoginError = UNKNOWN_ERROR;

    if(mHttpReq.startReq(accountVerifyAPI)) //auto-generating oauth header
    {
        if(parseXMLUserInfo(mHttpReq.getResult()))
        {
             if(Finalize())
             {
                 mLoginError = NO_ERROR;
                 return true;
             }
        }

        mLoginError = ACCOUNT_UNRECOGNIZED_DATA;
        #ifdef LOGIN_LOG
        LOGMAN.logError("When CLogin::getUserInfo , parsing info failed");
        #endif
    }
    else
    {
        if(mHttpReq.httpErrCode==mHttpReq.NOT_AUTHORIZED)
        {
            mLoginError = ACCOUNT_UNAUTHORIZED;
            #ifdef LOGIN_LOG
            LOGMAN.logError("When CLogin::getUserInfo , account is unauthorized.");
            #endif
        }
        else if(mHttpReq.httpErrCode==-1 || mHttpReq.httpErrCode==0)
        {
            mLoginError = HTTP_DISCONNECTED;

            #ifdef LOGIN_LOG
            LOGMAN.logError("When CLogin::getUserInfo , http req failed");
            #endif

            #ifdef HTTP_WTF
            if(mHttpReq.httpErrCode==0)
                LOGMAN.logInfo(wxString("No respond URL : ")<<mHttpReq.errorInfo);
            #endif
        }
    }

    return false;
}

void CLogin::logout()
{
    mOauth.resetAccessPair();
    mOauth.resetData();
}
